package org.carabiner.infinitest.depgraph;

import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Iterator;


/**
 * The <code>ClassFileParser</code> class is responsible for
 * parsing a Java class file to create a <code>JavaClass</code>
 * instance.
 *
 * @author <b>Mike Clark</b>
 * @author Clarkware Consulting, Inc.
 */

class ClassFileParser extends AbstractParser
{

  public static final int JAVA_MAGIC = 0xCAFEBABE;
  public static final int CONSTANT_UTF8 = 1;
  public static final int CONSTANT_UNICODE = 2;
  public static final int CONSTANT_INTEGER = 3;
  public static final int CONSTANT_FLOAT = 4;
  public static final int CONSTANT_LONG = 5;
  public static final int CONSTANT_DOUBLE = 6;
  public static final int CONSTANT_CLASS = 7;
  public static final int CONSTANT_STRING = 8;
  public static final int CONSTANT_FIELD = 9;
  public static final int CONSTANT_METHOD = 10;
  public static final int CONSTANT_INTERFACEMETHOD = 11;
  public static final int CONSTANT_NAMEANDTYPE = 12;
  public static final char CLASS_DESCRIPTOR = 'L';
  public static final int ACC_INTERFACE = 0x200;
  public static final int ACC_ABSTRACT = 0x400;

  private String fileName;
  private String className;
  private String superClassName;
  private String interfaceNames[];
  private boolean isAbstract;
  private JavaClass jClass;
  private Constant[] constantPool;
  private FieldOrMethodInfo[] fields;
  private FieldOrMethodInfo[] methods;
  private AttributeInfo[] attributes;
  private DataInputStream in;

  public ClassFileParser()
  {
    this(new PackageFilter());
  }

  public ClassFileParser(PackageFilter filter)
  {
    super(filter);
    reset();
  }

  private void reset()
  {
    className = null;
    superClassName = null;
    interfaceNames = new String[0];
    isAbstract = false;

    jClass = null;
    constantPool = new Constant[1];
    fields = new FieldOrMethodInfo[0];
    methods = new FieldOrMethodInfo[0];
    attributes = new AttributeInfo[0];
  }

  /**
   * Registered parser listeners are informed that the resulting
   * <code>JavaClass</code> was parsed.
   *
   * @param classFile File
   * @throws IOException
   * @return JavaClass
   */
  public JavaClass parse(File classFile) throws IOException
  {

    this.fileName = classFile.getCanonicalPath();

    debug("\nParsing " + fileName + "...");

    FileInputStream in = null;

    try
    {

      in = new FileInputStream(classFile);

      return parse(in);

    }
    finally
    {
      if (in != null)
      {
        try
        {
          in.close();
        }
        catch (IOException ioe)
        {
          ioe.printStackTrace();
        }
      }
    }
  }

  public JavaClass parse(InputStream is) throws IOException
  {

    reset();

    jClass = new JavaClass("Unknown");

    in = new DataInputStream(is);

    int magic = parseMagic();

    int minorVersion = parseMinorVersion();
    int majorVersion = parseMajorVersion();

    constantPool = parseConstantPool();

    parseAccessFlags();

    className = parseClassName();

    superClassName = parseSuperClassName();

    interfaceNames = parseInterfaces();

    fields = parseFields();

    methods = parseMethods();

    parseAttributes();

    addClassConstantReferences();

    onParsedJavaClass(jClass);

    return jClass;
  }

  private int parseMagic() throws IOException
  {
    int magic = in.readInt();
    if (magic != JAVA_MAGIC)
    {
      throw new IOException("Invalid class file: " + fileName);
    }

    return magic;
  }

  private int parseMinorVersion() throws IOException
  {
    return in.readUnsignedShort();
  }

  private int parseMajorVersion() throws IOException
  {
    return in.readUnsignedShort();
  }

  private Constant[] parseConstantPool() throws IOException
  {
    int constantPoolSize = in.readUnsignedShort();

    Constant[] pool = new Constant[constantPoolSize];

    for (int i = 1; i < constantPoolSize; i++)
    {

      Constant constant = parseNextConstant();

      pool[i] = constant;

      //
      // 8-byte constants use two constant pool entries
      //
      if (constant.getTag() == CONSTANT_DOUBLE
          || constant.getTag() == CONSTANT_LONG)
      {
        i++;
      }
    }

    return pool;
  }

  private void parseAccessFlags() throws IOException
  {
    int accessFlags = in.readUnsignedShort();

    boolean isAbstract = ( (accessFlags & ACC_ABSTRACT) != 0);
    boolean isInterface = ( (accessFlags & ACC_INTERFACE) != 0);

    this.isAbstract = isAbstract || isInterface;
    jClass.isAbstract(this.isAbstract);

    debug("Parser: abstract = " + this.isAbstract);
  }

  private String parseClassName() throws IOException
  {
    int entryIndex = in.readUnsignedShort();
    String className = getClassConstantName(entryIndex);
    jClass.setName(className);
    jClass.setPackageName(getPackageName(className));

    debug("Parser: class name = " + className);
    debug("Parser: package name = " + getPackageName(className));

    return className;
  }

  private String parseSuperClassName() throws IOException
  {
    int entryIndex = in.readUnsignedShort();
    String superClassName = getClassConstantName(entryIndex);
    addImport(getPackageName(superClassName));

    debug("Parser: super class name = " + superClassName);

    return superClassName;
  }

  private String[] parseInterfaces() throws IOException
  {
    int interfacesCount = in.readUnsignedShort();
    String[] interfaceNames = new String[interfacesCount];
    for (int i = 0; i < interfacesCount; i++)
    {
      int entryIndex = in.readUnsignedShort();
      interfaceNames[i] = getClassConstantName(entryIndex);
      addImport(getPackageName(interfaceNames[i]));

      debug("Parser: interface = " + interfaceNames[i]);
    }

    return interfaceNames;
  }

  private FieldOrMethodInfo[] parseFields() throws IOException
  {
    int fieldsCount = in.readUnsignedShort();
    FieldOrMethodInfo[] fields = new FieldOrMethodInfo[fieldsCount];
    for (int i = 0; i < fieldsCount; i++)
    {
      fields[i] = parseFieldOrMethodInfo();
      String descriptor = toUTF8(fields[i].getDescriptorIndex());
      debug("Parser: field descriptor = " + descriptor);
      String[] types = descriptorToTypes(descriptor);
      for (int t = 0; t < types.length; t++)
      {
        addImport(getPackageName(types[t]));
        debug("Parser: field type = " + types[t]);
      }
    }

    return fields;
  }

  private FieldOrMethodInfo[] parseMethods() throws IOException
  {
    int methodsCount = in.readUnsignedShort();
    FieldOrMethodInfo[] methods = new FieldOrMethodInfo[methodsCount];
    for (int i = 0; i < methodsCount; i++)
    {
      methods[i] = parseFieldOrMethodInfo();
      String descriptor = toUTF8(methods[i].getDescriptorIndex());
      debug("Parser: method descriptor = " + descriptor);
      String[] types = descriptorToTypes(descriptor);
      for (int t = 0; t < types.length; t++)
      {
        if (types[t].length() > 0)
        {
          addImport(getPackageName(types[t]));
          debug("Parser: method type = " + types[t]);
        }
      }
    }

    return methods;
  }

  private Constant parseNextConstant() throws IOException
  {

    Constant result;

    byte tag = in.readByte();

    switch (tag)
    {

      case (ClassFileParser.CONSTANT_CLASS):
      case (ClassFileParser.CONSTANT_STRING):
        result = new Constant(tag, in.readUnsignedShort());
        break;
      case (ClassFileParser.CONSTANT_FIELD):
      case (ClassFileParser.CONSTANT_METHOD):
      case (ClassFileParser.CONSTANT_INTERFACEMETHOD):
      case (ClassFileParser.CONSTANT_NAMEANDTYPE):
        result = new Constant(tag, in.readUnsignedShort(), in
                              .readUnsignedShort());
        break;
      case (ClassFileParser.CONSTANT_INTEGER):
        result = new Constant(tag, new Integer(in.readInt()));
        break;
      case (ClassFileParser.CONSTANT_FLOAT):
        result = new Constant(tag, new Float(in.readFloat()));
        break;
      case (ClassFileParser.CONSTANT_LONG):
        result = new Constant(tag, new Long(in.readLong()));
        break;
      case (ClassFileParser.CONSTANT_DOUBLE):
        result = new Constant(tag, new Double(in.readDouble()));
        break;
      case (ClassFileParser.CONSTANT_UTF8):
        result = new Constant(tag, in.readUTF());
        break;
      default:
        throw new IOException("Unknown constant: " + tag);
    }

    return result;
  }

  private FieldOrMethodInfo parseFieldOrMethodInfo() throws IOException
  {

    FieldOrMethodInfo result = new FieldOrMethodInfo(
      in.readUnsignedShort(), in.readUnsignedShort(), in
      .readUnsignedShort());

    int attributesCount = in.readUnsignedShort();
    for (int a = 0; a < attributesCount; a++)
    {
      parseAttribute();
    }

    return result;
  }

  private void parseAttributes() throws IOException
  {
    int attributesCount = in.readUnsignedShort();
    attributes = new AttributeInfo[attributesCount];

    for (int i = 0; i < attributesCount; i++)
    {
      attributes[i] = parseAttribute();

      // Section 4.7.7 of VM Spec - Class File Format
      if (attributes[i].getName() != null)
      {
        if (attributes[i].getName().equals("SourceFile"))
        {
          byte[] b = attributes[i].getValue();
          int b0 = b[0] < 0 ? b[0] + 256 : b[0];
          int b1 = b[1] < 0 ? b[1] + 256 : b[1];
          int pe = b0 * 256 + b1;

          String descriptor = toUTF8(pe);
          jClass.setSourceFile(descriptor);
        }
      }
    }
  }

  private AttributeInfo parseAttribute() throws IOException
  {
    AttributeInfo result = new AttributeInfo();

    int nameIndex = in.readUnsignedShort();
    if (nameIndex != -1)
    {
      result.setName(toUTF8(nameIndex));
    }

    int attributeLength = in.readInt();
    byte[] value = new byte[attributeLength];
    for (int b = 0; b < attributeLength; b++)
    {
      value[b] = in.readByte();
    }

    result.setValue(value);
    return result;
  }

  private Constant getConstantPoolEntry(int entryIndex) throws IOException
  {

    if (entryIndex < 0 || entryIndex >= constantPool.length)
    {
      throw new IOException("Illegal constant pool index : " + entryIndex);
    }

    return constantPool[entryIndex];
  }

  private void addClassConstantReferences() throws IOException
  {
    for (int j = 1; j < constantPool.length; j++)
    {
      if (constantPool[j].getTag() == CONSTANT_CLASS)
      {
        String name = toUTF8(constantPool[j].getNameIndex());
        addImport(name);

        debug("Parser: class type = " + slashesToDots(name));
      }

      if (constantPool[j].getTag() == CONSTANT_DOUBLE
          || constantPool[j].getTag() == CONSTANT_LONG)
      {
        j++;
      }
    }
  }

  private String getClassConstantName(int entryIndex) throws IOException
  {

    Constant entry = getConstantPoolEntry(entryIndex);
    if (entry == null)
    {
      return "";
    }
    return slashesToDots(toUTF8(entry.getNameIndex()));
  }

  private String toUTF8(int entryIndex) throws IOException
  {
    Constant entry = getConstantPoolEntry(entryIndex);
    if (entry.getTag() == CONSTANT_UTF8)
    {
      return (String) entry.getValue();
    }

    throw new IOException("Constant pool entry is not a UTF8 type: "
                          + entryIndex);
  }

  private void addImport(String importClass)
  {
    if (importClass != null && !importClass.contains(".") && getFilter().accept(importClass))
    {
      importClass = importClass.replace('/', '.');
      jClass.addImport(importClass);
    }
  }

  private String slashesToDots(String s)
  {
    return s.replace('/', '.');
  }

  private String getPackageName(String s)
  {
    if ( (s.length() > 0) && (s.charAt(0) == '['))
    {
      String types[] = descriptorToTypes(s);
      if (types.length == 0)
      {
        return null; // primitives
      }

      s = types[0];
    }

    s = slashesToDots(s);
    int index = s.lastIndexOf(".");
    if (index > 0)
    {
      return s.substring(0, index);
    }

    return "Default";
  }

  private String[] descriptorToTypes(String descriptor)
  {

    int typesCount = 0;
    for (int index = 0; index < descriptor.length(); index++)
    {
      if (descriptor.charAt(index) == ';')
      {
        typesCount++;
      }
    }

    String types[] = new String[typesCount];

    int typeIndex = 0;
    for (int index = 0; index < descriptor.length(); index++)
    {

      int startIndex = descriptor.indexOf(CLASS_DESCRIPTOR, index);
      if (startIndex < 0)
      {
        break;
      }

      index = descriptor.indexOf(';', startIndex + 1);
      types[typeIndex++] = descriptor.substring(startIndex + 1, index);
    }

    return types;
  }

  class Constant
  {

    private byte _tag;

    private int _nameIndex;

    private int _typeIndex;

    private Object _value;

    Constant(byte tag, int nameIndex)
    {
      this(tag, nameIndex, -1);
    }

    Constant(byte tag, Object value)
    {
      this(tag, -1, -1);
      _value = value;
    }

    Constant(byte tag, int nameIndex, int typeIndex)
    {
      _tag = tag;
      _nameIndex = nameIndex;
      _typeIndex = typeIndex;
      _value = null;
    }

    byte getTag()
    {
      return _tag;
    }

    int getNameIndex()
    {
      return _nameIndex;
    }

    int getTypeIndex()
    {
      return _typeIndex;
    }

    Object getValue()
    {
      return _value;
    }

    public String toString()
    {

      StringBuffer s = new StringBuffer("");

      s.append("tag: " + getTag());

      if (getNameIndex() > -1)
      {
        s.append(" nameIndex: " + getNameIndex());
      }

      if (getTypeIndex() > -1)
      {
        s.append(" typeIndex: " + getTypeIndex());
      }

      if (getValue() != null)
      {
        s.append(" value: " + getValue());
      }

      return s.toString();
    }
  }

  class FieldOrMethodInfo
  {

    private int _accessFlags;

    private int _nameIndex;

    private int _descriptorIndex;

    FieldOrMethodInfo(int accessFlags, int nameIndex, int descriptorIndex)
    {

      _accessFlags = accessFlags;
      _nameIndex = nameIndex;
      _descriptorIndex = descriptorIndex;
    }

    int accessFlags()
    {
      return _accessFlags;
    }

    int getNameIndex()
    {
      return _nameIndex;
    }

    int getDescriptorIndex()
    {
      return _descriptorIndex;
    }

    public String toString()
    {
      StringBuffer s = new StringBuffer("");

      try
      {

        s.append("\n    name (#" + getNameIndex() + ") = "
                 + toUTF8(getNameIndex()));

        s.append("\n    signature (#" + getDescriptorIndex() + ") = "
                 + toUTF8(getDescriptorIndex()));

        String[] types = descriptorToTypes(toUTF8(getDescriptorIndex()));
        for (int t = 0; t < types.length; t++)
        {
          s.append("\n        type = " + types[t]);
        }

      }
      catch (Exception e)
      {
        e.printStackTrace();
      }

      return s.toString();
    }
  }

  class AttributeInfo
  {

    private String name;

    private byte[] value;

    public void setName(String name)
    {
      this.name = name;
    }

    public String getName()
    {
      return this.name;
    }

    public void setValue(byte[] value)
    {
      this.value = value;
    }

    public byte[] getValue()
    {
      return this.value;
    }
  }

  /**
   * Returns a string representation of this object.
   *
   * @return String representation.
   */
  public String toString()
  {

    StringBuffer s = new StringBuffer();

    try
    {

      s.append("\n" + className + ":\n");

      s.append("\nConstants:\n");
      for (int i = 1; i < constantPool.length; i++)
      {
        Constant entry = getConstantPoolEntry(i);
        s.append("    " + i + ". " + entry.toString() + "\n");
        if (entry.getTag() == CONSTANT_DOUBLE
            || entry.getTag() == CONSTANT_LONG)
        {
          i++;
        }
      }

      s.append("\nClass Name: " + className + "\n");
      s.append("Super Name: " + superClassName + "\n\n");

      s.append(interfaceNames.length + " interfaces\n");
      for (int i = 0; i < interfaceNames.length; i++)
      {
        s.append("    " + interfaceNames[i] + "\n");
      }

      s.append("\n" + fields.length + " fields\n");
      for (int i = 0; i < fields.length; i++)
      {
        s.append(fields[i].toString() + "\n");
      }

      s.append("\n" + methods.length + " methods\n");
      for (int i = 0; i < methods.length; i++)
      {
        s.append(methods[i].toString() + "\n");
      }

      s.append("\nDependencies:\n");
      Iterator imports = jClass.getImports().iterator();
      while (imports.hasNext())
      {
        String jPackage = (String) imports.next();
        s.append("    " + jPackage + "\n");
      }

    }
    catch (Exception e)
    {
      e.printStackTrace();
    }

    return s.toString();
  }

  /**
   * Test main.
   *
   * @param args String[]
   */
  public static void main(String args[])
  {
    try
    {
      DEBUG = true;

      if (args.length <= 0)
      {
        System.err.println("usage: ClassFileParser <class-file>");
        System.exit(0);
      }

      ClassFileParser parser = new ClassFileParser();

      parser.parse(new File(args[0]));

      System.err.println(parser.toString());

    }
    catch (Exception e)
    {
      System.err.println(e.getMessage());
    }
  }
}
